/**
 * Copyright 2009 Joe LaPenna
 */

package com.googlecode.dumpcatcher.logging;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SignatureException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * @author Joe LaPenna (joe@joelapenna.com)
 */
public class Dumpcatcher {

    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private static final Comparator<NameValuePair> NAME_VALUE_PAIR_COMPARATOR = new NameValuePairComparator();
    private static final String HMAC_SHA1 = "HmacSHA1";

    private String mProductKey;
    private String mSecret;
    private DefaultHttpClient mHttpClient;
    private String mUrl;
    private String mClient;

    public Dumpcatcher(String productKey, String secret, String url, String client, int timeout) {
        mProductKey = productKey;
        mSecret = secret;
        mUrl = url;
        mClient = client;
        mHttpClient = createHttpClient(timeout);
    }

    public HttpResponse sendCrash(NameValuePair... parameters) throws IOException {
        try {
            ArrayList<NameValuePair> pairs = addExtraQueryPairsAndSign(parameters);
            UrlEncodedFormEntity hmacEntity = new UrlEncodedFormEntity(pairs, HTTP.UTF_8);

            HttpPost httpPost = new HttpPost(mUrl);
            httpPost.setEntity(hmacEntity);
            HttpResponse response = mHttpClient.execute(httpPost);
            return response;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        } catch (SignatureException e) {
            throw new RuntimeException(e);
        }
    }

    public HttpResponse sendCrash(String shortMessage, String longMessage, String severity,
            String tag) throws IOException {
        return sendCrash( //
                new BasicNameValuePair("short", shortMessage), //
                new BasicNameValuePair("long", longMessage), //
                new BasicNameValuePair("severity", severity), //
                new BasicNameValuePair("tag", tag), //
                new BasicNameValuePair("date", Dumpcatcher.DATE_FORMAT.format(new Date())) //
        );
    }

    ArrayList<NameValuePair> addExtraQueryPairsAndSign(NameValuePair[] parameters)
            throws SignatureException {
        ArrayList<NameValuePair> pairs = new ArrayList<NameValuePair>();
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].getValue() != null) {
                pairs.add(parameters[i]);
            }
        }
        pairs.add(new BasicNameValuePair("product_key", mProductKey));
        pairs.add(new BasicNameValuePair("client", mClient));

        pairs.add(new BasicNameValuePair("hmac", calculateHMAC(pairs)));
        return pairs;
    }

    String calculateHMAC(List<NameValuePair> parameters) throws SignatureException {
        // sorted string of key/value pairs to hash.
        Collections.sort(parameters, NAME_VALUE_PAIR_COMPARATOR);
        StringBuffer sb = new StringBuffer();
        int almostLast = parameters.size() - 1;
        for (int i = 0; i < almostLast; i++) {
            sb.append(((NameValuePair)parameters.get(i)).toString());
            sb.append('&');
        }
        // Add the last guy, no ampersand.
        sb.append(((NameValuePair)parameters.get(parameters.size() - 1)).toString());

        // Convert to string
        String message = sb.toString();
        return calculateRFC2104HMAC(message, mSecret);
    }

    /**
     * Computes RFC 2104-compliant HMAC signature.
     *
     * @param data The data to be signed.
     * @param key The signing key.
     * @return The hex digest of the RFC 2104-compliant HMAC signature.
     * @throws java.security.SignatureException when signature generation fails
     */
    String calculateRFC2104HMAC(String data, String key) throws java.security.SignatureException {
        Logger.getLogger(("Dumpcatcher")).log(Level.INFO, data);
        try {
            String result;
            // get an hmac_sha1 key from the raw key bytes
            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1);

            // get an hmac_sha1 Mac instance and initialize with the signing key
            Mac mac = Mac.getInstance(HMAC_SHA1);
            mac.init(signingKey);

            // compute the hmac on input data bytes
            byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8"));
            result = new BigInteger(1, rawHmac).toString(16);
            return result;

        } catch (Exception e) {
            throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
        }
    }

    /**
     * Create a thread-safe client. This client does not do redirecting, to allow us to capture
     * correct "error" codes.
     *
     * @return HttpClient
     */
    static final DefaultHttpClient createHttpClient(int timeout) {
        // Sets up the http part of the service.
        final SchemeRegistry supportedSchemes = new SchemeRegistry();

        // Register the "http" protocol scheme, it is required
        // by the default operator to look up socket factories.
        final SocketFactory sf = PlainSocketFactory.getSocketFactory();
        supportedSchemes.register(new Scheme("http", sf, 80));

        // Since dumpcatcher only supports ssl right now, use that.
        final SocketFactory sslSf = SSLSocketFactory.getSocketFactory();
        ((SSLSocketFactory)sslSf).setHostnameVerifier(new StrictHostnameVerifier());
        supportedSchemes.register(new Scheme("https", sslSf, 443));

        // Set some client http client parameter defaults.
        final HttpParams httpParams = createHttpParams(timeout);
        HttpClientParams.setRedirecting(httpParams, false);

        final ClientConnectionManager ccm = new ThreadSafeClientConnManager(httpParams,
                supportedSchemes);
        return new DefaultHttpClient(ccm, httpParams);
    }

    /**
     * Create the default HTTP protocol parameters.
     */
    private static final HttpParams createHttpParams(int timeout) {
        final HttpParams params = new BasicHttpParams();

        // Turn off stale checking. Our connections break all the time anyway,
        // and it's not worth it to pay the penalty of checking every time.
        HttpConnectionParams.setStaleCheckingEnabled(params, false);

        HttpConnectionParams.setConnectionTimeout(params, timeout * 1000);
        HttpConnectionParams.setSoTimeout(params, timeout * 1000);
        HttpConnectionParams.setSocketBufferSize(params, 8192);

        return params;
    }

    private static final class NameValuePairComparator implements Comparator<NameValuePair> {
        @Override
        public int compare(NameValuePair x, NameValuePair y) {
            return x.getName().compareTo(y.getName());
        }
    }

}
